

Polling 有兩個明顯的缺點:
Long Polling解決了這兩個問題。
雖然行為概觀上與Polling一樣,是一次Request後得到Response,才再發出一個Request。但是Reponse的回傳可能會拉的老長,過了許久才回應,像彗星(Comet)一樣。也確實有一種方式就叫做彗星(Comet),Long Polling是他的變種[^6]。
在Long Polling模式下,有一個長期連線,這個連線在資料一更新時,變會回傳Response,並結束此輪連線,然後再發起一次長連線請求,以做到即時更新的效果。
有一段時間在Facebook、Plurk可以見得此種方式。甚至現在還在Facebook、Plurk還是可以見得一些Request長時間沒有Response,不過我無法確定是否同為Long Polling。
下圖是Plurk的部份網路請求節圖。其中可以看到一個/connect相關的請求,並過一段時間後才返回Response,隨後又建立一比連線:

與Polling一樣,應用架構或許需要微調,但不需要調整多少,且多數瀏覽器支援。並且實質意義上做到即時更新的效果。
Long Polling長期佔用連線。一般對於伺服器而言,有連線數上限的問題。在使用與建構上需要考量水平服務擴張的能力。
此外,每次更新後,都需要再重新發起一次連線。儘管較少,但同樣有多次TCP的三段式交握連線的負擔。
要建立Long Polling的實驗環境就不能簡單靠Nginx、Apach Web Sever等網頁伺服器了。會需要一個網站應用框架,這裡選擇FastAPI,因此你需要有Python的環境,然後安裝fastapi:
pip install fastapi uvicorn watchdog
沿用Polling的index.html並做一些調整修改。
<!-- www-data/index.html -->
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<title>即時更新內容 - Long Polling</title>
</head>
<body>
<h1 id="content"></h1>
</body>
<script defer type="module">
const delay = 0 /*ms*/;
const contentEl = document.querySelector('#content');
let timer = null;
let lastUpdate = undefined;
async function updateContent(){
let headers = {};
headers['If-Modified-Since'] = lastUpdate;
try {
let response = await fetch('/connect', { headers } )
const data = await response.json();
lastUpdate = data.lastModified;
contentEl.innerText = data.message;
} catch (e) {
console.error(e);
}
timer = setTimeout( updateContent , delay);
}
updateContent();
</script>
</html>
首先是delay不再需要了,因此設為0。在一次更新結束後,立刻建立一個長連線。
這次的回傳訊息,除了內容本身外,有一個更新時間。現在必須儲存下來,用以告知伺服器訂閱更新的需求。添加lastUpdate用於記錄。在請求的時候,簡單的將記錄添加於請求頭中:
headers['If-Modified-Since'] = lastUpdate;
稍微把Endpoint也換個名稱--/connect表明用於長連線,訂閱更新訊息。
在收到回應後(Response),訊息本身(message)同樣顯示於畫面之上。並且更新lastUpdate。
引入一些必要的package。這裡會使用watchdog去監看檔案是否有被修改。
from typing import Union, TypedDict
from fastapi import FastAPI, Header, Response
from fastapi.responses import HTMLResponse, FileResponse
from watchdog.events import LoggingEventHandler
from watchdog.observers import Observer
import os
import time
然後是一些常數、變數。
CONTENT_FILE = 'content.txt'
GMT_FORMAT = '%a, %d %b %Y %H:%M:%S GMT'
app = FastAPI()
接著簡單的提供index.html檔案。
@app.get('/index.html', response_class=HTMLResponse)
async def index():
return FileResponse('index.html')
通常而言靜態檔案不會由應用框架提供,會直接由專門的Web Server提供。在FastAPI也有關於靜態檔案的處理方式。只是這裡簡化處理。
然後是重頭戲/connect的部分。會先比對檔案是否有被更改過:
@app.get('/connect')
def connect(response: Response,
if_modified_since: Union[str, None] = Header(default=None)):
mtime = os.path.getmtime(CONTENT_FILE)
mtime = time.gmtime(mtime)
last_modified = time.strftime(GMT_FORMAT, mtime)
if if_modified_since is None or \
if_modified_since != last_modified:
pass
更改的時間是否與要求一致?是的話表示並沒有被更改過,那就可以直接回傳內容:
msg = ''
with open(CONTENT_FILE) as f:
msg = f.read();
return {'lastModified': last_modified, 'message': msg}
否則的話就去監看檔案,直到檔案被修改都保持著連線:
mtime = watch(CONTENT_FILE) # 這裡會保持連線
mtime = time.gmtime(mtime)
last_modified = time.strftime(GMT_FORMAT, mtime)
特別是watch是當檔案被更改後才會繼續執行後續動作:
class FileStat(TypedDict):
modified: bool = False
def watch(file_path: str):
# ......
try:
while file_stat["modified"] is False:
time.sleep(1)
# ......
observer.stop()
return os.path.getmtime(CONTENT_FILE)
對於watchdog,只是簡單的監聽更改事件,當檔案有所變動後,就會調整參考的狀態:
def watch(file_path: str):
file_stat: FileStat = { "modified": False }
observer = Observer()
observer.schedule(WatchDogEvent(file_stat), CONTENT_FILE, recursive=False)
observer.start()
# ......
try:
# ......
# ......
observer.stop()
return os.path.getmtime(CONTENT_FILE)
class WatchDogEvent(LoggingEventHandler):
def __init__(self, file_stat: FileStat):
self.file_stat: FileStat = file_stat
def on_modified(self, event):
self.file_stat["modified"] = True
最後同樣地,將結果回傳給瀏覽器:
@app.get('/connect')
def connect(response: Response,
if_modified_since: Union[str, None] = Header(default=None)):
# ......
msg = ''
with open(CONTENT_FILE) as f:
msg = f.read();
return {'lastModified': last_modified, 'message': msg}
現在可以嘗試啓動服務器看看
uvicorn app:app
開啓瀏覽器瀏覽 http://localhost:8000/index.html

你會注意到,總是更新過後才有一個新的Request。這樣子減少了來回的次數,並提高了即時性。
[^6]: 獲得實時更新的方法(Polling, Comet, Long Polling, WebSocket)。取用時間:2022.09.04。
本文同時發表於我的隨筆